﻿namespace MSSqlSchemaDoc.Xslt
{
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Reflection;
    using System.Resources;
    using System.Xml;
    using System.Xml.Xsl;
    using MSSqlSchemaDoc.Xslt.Resources;

    /// <summary>
    /// Handles the conversion of Xml to report files.
    /// </summary>
    public class ReportGenerator
    {
        /// <summary>
        /// The list of stylesheets extracted from the resource file.
        /// </summary>
        private IDictionary<string, string> stylesheetPaths;

        /// <summary>
        /// Settings for Xsl transformations.
        /// </summary>
        private XsltSettings xsltSettings;

        /// <summary>
        /// The Xslt compiled transformation object.
        /// </summary>
        private IDictionary<string, XslCompiledTransform> xsltCompiledTransformObjects;

        /// <summary>
        /// The destination folder for the Html reports.
        /// </summary>
        private string reportObjectsFolder;

        /// <summary>
        /// The stream to which to write status messages.
        /// </summary>
        private TextWriter statusStream;

        /// <summary>
        /// Initializes a new instance of the ReportGenerator class.
        /// </summary>
        public ReportGenerator()
        {
            this.xsltCompiledTransformObjects = new Dictionary<string, XslCompiledTransform>();
            this.stylesheetPaths = new Dictionary<string, string>();
            this.xsltSettings = new XsltSettings(true, false);
        }

        /// <summary>
        /// Generates the reports.
        /// </summary>
        /// <param name="schemaFolder">The folder containing the Xml structure schema files.</param>
        /// <param name="documentationFolder">The folder containing the Xml documentation files.</param>
        /// <param name="htmlReportFolder">The destination folder for the Html report files.</param>
        /// <param name="statusStream">The stream to which to write status messages.</param>
        /// <param name="connectionFriendlyName">The friendly name of the connection.</param>
        public static void GenerateReport(DirectoryInfo schemaFolder, DirectoryInfo documentationFolder, DirectoryInfo htmlReportFolder, TextWriter statusStream, string connectionFriendlyName)
        {
            ReportGenerator rg = new ReportGenerator();
            rg.statusStream = statusStream;
            rg.Generate(schemaFolder, documentationFolder, htmlReportFolder, connectionFriendlyName);
        }

        /// <summary>
        /// A helper method to write text to a specified text writer object.
        /// </summary>
        /// <param name="writer">An instance of a text writer object.</param>
        /// <param name="text">A composite format string.</param>
        /// <param name="values">A string array containing zero or more strings to format.</param>
        private static void WriteLine(TextWriter writer, string text, params string[] values)
        {
            writer.WriteLine(
                string.Format(
                    CultureInfo.CurrentUICulture,
                    text,
                    values));
        }

        /// <summary>
        /// Generates the reports.
        /// </summary>
        /// <param name="schemaFolder">The folder containing the Xml structure schema files.</param>
        /// <param name="documentationFolder">The folder containing the Xml documentation files.</param>
        /// <param name="htmlReportFolder">The destination folder for the Html report files.</param>
        /// <param name="connectionFriendlyName">The friendly name of the connection.</param>
        private void Generate(DirectoryInfo schemaFolder, DirectoryInfo documentationFolder, DirectoryInfo htmlReportFolder, string connectionFriendlyName)
        {
            if (!htmlReportFolder.Exists)
            {
                htmlReportFolder.Create();
            }

            // Load Xsl files
            this.LoadXslStylesheets(htmlReportFolder);
            this.SetupReportFolder(htmlReportFolder);

            WriteLine(this.statusStream, "[Building documentation file list]", new string[] { });
            IDictionary<string, XmlFileInfo> documentationFiles = this.BuildFileList(documentationFolder);

            WriteLine(this.statusStream, "[Building schema file list]", new string[] { });
            IDictionary<string, XmlFileInfo> schemaFiles = this.BuildFileList(schemaFolder);

            foreach (KeyValuePair<string, XmlFileInfo> kvp in schemaFiles)
            {
                WriteLine(this.statusStream, "[Transforming object: {0}{1}{2} ({3})]", new string[] { kvp.Value.Schema, string.IsNullOrEmpty(kvp.Value.Schema) ? string.Empty : ".", kvp.Value.Name, kvp.Value.Type });
                this.TransformFile(kvp.Value, documentationFiles, connectionFriendlyName);
            }

            WriteLine(this.statusStream, "[Generating index file]", new string[] { });
            this.GenerateIndexFile(htmlReportFolder, schemaFiles, connectionFriendlyName);

            WriteLine(this.statusStream, "[Cleaning up]", new string[] { });
            foreach (string path in this.stylesheetPaths.Values)
            {
                File.Delete(path);
            }
        }

        /// <summary>
        /// Creates the XslCompiledTransform object and sets the required options.
        /// </summary>
        /// <param name="reportFolder">The root folder for the reports.</param>
        private void LoadXslStylesheets(DirectoryInfo reportFolder)
        {
            // Add all available XSL stylesheet resources
            Assembly assembly = Assembly.GetExecutingAssembly();
            ResourceManager resMan = Resources.XsltStylesheets.ResourceManager;
            foreach (string resource in assembly.GetManifestResourceNames())
            {
                if (resource == string.Concat(resMan.BaseName, ".resources"))
                {
                    ResourceReader resReader = new ResourceReader(assembly.GetManifestResourceStream(resource));
                    IDictionaryEnumerator enumerator = resReader.GetEnumerator();

                    while (enumerator.MoveNext())
                    {
                        string key = enumerator.Key.ToString();

                        // Save the stylesheet to the root of the report folder and add it to the collection in order to remove it afterwards
                        string stylesheetName = Path.Combine(reportFolder.FullName, string.Concat(key, ".xsl"));
                        this.stylesheetPaths.Add(key, stylesheetName);

                        using (FileStream fs = new FileStream(stylesheetName, FileMode.Create))
                        using (StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.UTF8))
                        {
                            sw.Write(resMan.GetString(key));
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Sets up the report folder.
        /// </summary>
        /// <param name="reportFolder">The main report folder.</param>
        private void SetupReportFolder(DirectoryInfo reportFolder)
        {
            // Copy the Css stylesheet
            using (FileStream fs = new FileStream(Path.Combine(reportFolder.FullName, "main.css"), FileMode.Create))
            using (StreamWriter sw = new StreamWriter(fs))
            {
                sw.Write(HtmlSupportFiles.MainCss);
            }

            this.reportObjectsFolder = reportFolder.CreateSubdirectory("objects").FullName;
        }

        /// <summary>
        /// Recursively builds a list of files in the folder.
        /// </summary>
        /// <param name="path">The path to recurse.</param>
        /// <returns>The list of files with information for each.</returns>
        private IDictionary<string, XmlFileInfo> BuildFileList(DirectoryInfo path)
        {
            IDictionary<string, XmlFileInfo> list = new Dictionary<string, XmlFileInfo>();

            if (path.Exists)
            {
                foreach (FileInfo file in path.GetFiles())
                {
                    using (XmlTextReader xmlReader = new XmlTextReader(file.FullName))
                    {
                        xmlReader.MoveToContent();
                        XmlFileInfo info = new XmlFileInfo()
                            {
                                Type = xmlReader.LocalName,
                                Name = xmlReader.GetAttribute("Name"),
                                Schema = xmlReader.GetAttribute("Schema"),
                                FullPath = file.FullName
                            };
                        string key = string.Concat(info.Type, "_", info.Name, "_", info.Schema);
                        list.Add(key, info);
                    }
                }

                foreach (DirectoryInfo dir in path.GetDirectories())
                {
                    foreach (KeyValuePair<string, XmlFileInfo> kvp in this.BuildFileList(dir))
                    {
                        list.Add(kvp);
                    }
                }
            }

            return list;
        }

        /// <summary>
        /// Applies the Xsl transformations to the file.
        /// </summary>
        /// <param name="fileInfo">The schema file info.</param>
        /// <param name="documentationFiles">The list of documentation files.</param>
        /// <param name="connectionFriendlyName">The friendly name of the connection.</param>
        private void TransformFile(XmlFileInfo fileInfo, IDictionary<string, XmlFileInfo> documentationFiles, string connectionFriendlyName)
        {
            if (!this.xsltCompiledTransformObjects.ContainsKey(fileInfo.Type))
            {
                XslCompiledTransform xslt = new XslCompiledTransform(true);
                xslt.Load(this.stylesheetPaths[fileInfo.Type], this.xsltSettings, new XmlUrlResolver());
                this.xsltCompiledTransformObjects.Add(fileInfo.Type, xslt);
            }

            fileInfo.DocumentationFileKey = string.Concat(fileInfo.Type, "Documentation_", fileInfo.Name, "_", fileInfo.Schema);
            fileInfo.ResultFileName = string.Concat(fileInfo.Type, "_", Path.GetFileNameWithoutExtension(fileInfo.FullPath), ".html");
            fileInfo.ResultFileFullPath = Path.Combine(this.reportObjectsFolder, fileInfo.ResultFileName);

            XsltArgumentList args = new XsltArgumentList();

            if (documentationFiles.ContainsKey(fileInfo.DocumentationFileKey))
            {
                args.AddParam("DocumentationFile", string.Empty, documentationFiles[fileInfo.DocumentationFileKey].FullPath);
            }
            else
            {
                args.AddParam("DocumentationFile", string.Empty, string.Empty);
            }

            using (FileStream fs = new FileStream(fileInfo.ResultFileFullPath, FileMode.Create))
            {
                this.xsltCompiledTransformObjects[fileInfo.Type].Transform(fileInfo.FullPath, args, fs);
            }
        }

        /// <summary>
        /// Generates an index file with pointers to all the Html report files.
        /// </summary>
        /// <param name="reportFolder">The main report folder.</param>
        /// <param name="fileList">The file list.</param>
        /// <param name="connectionFriendlyName">The friendly name of the connection to be used for the CHM project name</param>
        private void GenerateIndexFile(DirectoryInfo reportFolder, IDictionary<string, XmlFileInfo> fileList, string connectionFriendlyName)
        {
            // Create a list and sort it appropriately
            List<XmlFileInfo> files = new List<XmlFileInfo>(fileList.Values);
            files.Sort();

            // Count the amount of objects per object type
            IDictionary<string, int> countPerType = new Dictionary<string, int>();
            foreach (XmlFileInfo info in files)
            {
                if (countPerType.ContainsKey(info.Type))
                {
                    countPerType[info.Type] += 1;
                }
                else
                {
                    countPerType.Add(info.Type, 1);
                }
            }

            using (FileStream fs = new FileStream(Path.Combine(reportFolder.FullName, "index.html"), FileMode.Create))
            using (StreamWriter sw = new StreamWriter(fs))
            {
                sw.WriteLine("<html>");
                sw.WriteLine("<head>");
                sw.WriteLine(string.Concat("<title>", connectionFriendlyName, "</title>"));
                sw.WriteLine("<link rel='stylesheet' type='text/css' href='main.css'/>");
                sw.WriteLine("</head>");
                sw.WriteLine("<body>");
                sw.WriteLine("<table width='100%'>");

                sw.WriteLine(string.Concat("<tr><td colspan='3' class='descriptionHeader center'>", connectionFriendlyName, " - Index Of Objects</td></tr>"));

                string previousType = string.Empty;
                int loopCounter = 0; // The overall loop counter
                int typeStartCounter;    // The counter in the current object type

                while (loopCounter < files.Count)
                {
                    XmlFileInfo info = files[loopCounter];

                    // Start a new type
                    typeStartCounter = loopCounter;
                    sw.WriteLine("<tr><td class='blank noborder' width='34%'>&nbsp;</td><td class='blank noborder' width='34%'>&nbsp;</td><td class='blank noborder'>&nbsp;</td></tr>");
                    sw.WriteLine(string.Concat("<tr><td colspan='3' class='descriptionHeaderMedium noborder center'>", info.Type, " List</td></tr>"));
                    sw.WriteLine("<tr class='center'>");

                    string[] columns = this.BuildColumnsForType(files, ref loopCounter, countPerType[info.Type]);
                    sw.WriteLine(string.Concat("<td class='top'>", columns[0], "</td>"));
                    sw.WriteLine(string.Concat("<td class='top'>", columns[1], "</td>"));
                    sw.WriteLine(string.Concat("<td class='top'>", columns[2], "</td>"));

                    sw.WriteLine("</tr>");

                    // Close off this type
                    previousType = files[loopCounter - 1].Type;
                }

                sw.WriteLine("</table>");
                sw.WriteLine("</body>");
                sw.WriteLine("</html>");
            }

            this.GenerateChmFiles(reportFolder, files, connectionFriendlyName);
        }

        /// <summary>
        /// Generates the CHM support files.
        /// </summary>
        /// <param name="reportFolder">The main report folder.</param>
        /// <param name="fileList">The file list.</param>
        /// <param name="connectionFriendlyName">The friendly name of the connection to be used for the CHM project name</param>
        private void GenerateChmFiles(DirectoryInfo reportFolder, List<XmlFileInfo> fileList, string connectionFriendlyName)
        {
            System.Text.StringBuilder file = new System.Text.StringBuilder();

            file.AppendLine("[OPTIONS]");
            file.AppendLine("Compatibility=1.1 or later");
            file.AppendLine(string.Concat("Compiled file=", connectionFriendlyName, ".chm"));
            file.AppendLine(@"Default topic=index.html");
            file.AppendLine("Display compile progress=No");
            file.AppendLine(string.Concat("Index file=", connectionFriendlyName, "_Index.hhk"));
            file.AppendLine(string.Concat("Title=", connectionFriendlyName));
            file.AppendLine(string.Empty);
            file.AppendLine(string.Empty);
            file.AppendLine("[FILES]");
            file.AppendLine("index.html");
            file.AppendLine(string.Empty);
            file.AppendLine("[INFOTYPES]");
            file.AppendLine(string.Empty);

            using (FileStream fs = new FileStream(Path.Combine(reportFolder.FullName, string.Concat(connectionFriendlyName, "_Project.hhp")), FileMode.Create))
            using (StreamWriter sw = new StreamWriter(fs))
            {
                sw.Write(file.ToString());
            }

            file.Remove(0, file.Length);

            file.AppendLine("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">");
            file.AppendLine("<HTML>");
            file.AppendLine("<HEAD>");
            file.AppendLine("<meta name=\"GENERATOR\" content=\"Microsoft&reg; HTML Help Workshop 4.1\">");
            file.AppendLine("<!-- Sitemap 1.0 -->");
            file.AppendLine("</HEAD><BODY>");
            file.AppendLine("<UL>");

            // Add index file
            file.AppendLine("	<LI><OBJECT type=\"text/sitemap\">");
            file.AppendLine("		<param name=\"Name\" value=\"*Index*\">");
            file.AppendLine("		<param name=\"Local\" value=\"index.html\">");
            file.AppendLine("		</OBJECT></LI>");

            // Add other files
            foreach (XmlFileInfo fileInfo in fileList)
            {
                file.AppendLine("	<LI><OBJECT type=\"text/sitemap\">");
                file.Append("		<param name=\"Name\" value=\"");
                
                file.Append(fileInfo.Name);

                if (!string.IsNullOrEmpty(fileInfo.Schema))
                {
                    file.Append(string.Concat(" (", fileInfo.Schema, ")"));
                }

                file.Append(string.Concat(" (", fileInfo.Type, ")"));
                file.Append("\">");

                file.AppendLine(string.Concat("		<param name=\"Local\" value=\"objects\\", fileInfo.ResultFileName, "\">"));
                file.AppendLine("		</OBJECT></LI>");
            }

            file.AppendLine("</UL>");
            file.AppendLine("</BODY></HTML>");

            using (FileStream fs = new FileStream(Path.Combine(reportFolder.FullName, string.Concat(connectionFriendlyName, "_Index.hhk")), FileMode.Create))
            using (StreamWriter sw = new StreamWriter(fs))
            {
                sw.Write(file.ToString());
            }
        }

        /// <summary>
        /// Build the 3 columns of object names.
        /// </summary>
        /// <param name="files">The file list.</param>
        /// <param name="currentLoopPos">The current position in the list.</param>
        /// <param name="typeCount">The count of objects of the current type.</param>
        /// <returns>The columns.</returns>
        private string[] BuildColumnsForType(List<XmlFileInfo> files, ref int currentLoopPos, int typeCount)
        {
            int startCount = currentLoopPos;
            int[] columnSplitStart = new int[2];
            string[] cols = new string[3];
            XmlFileInfo info;

            columnSplitStart[0] = (typeCount / 3) + ((typeCount % 3) / 2) + ((typeCount % 3) % 2);
            columnSplitStart[1] = columnSplitStart[0] + (typeCount / 3) + ((typeCount % 3) / 2);

            columnSplitStart[0] += startCount;
            columnSplitStart[1] += startCount;

            cols[0] = "<table width='100%' class='center'>";
            cols[1] = cols[0];
            cols[2] = cols[0];

            System.Text.StringBuilder text;
            while (currentLoopPos < startCount + typeCount)
            {
                info = files[currentLoopPos];
                text = new System.Text.StringBuilder();

                text.Append("<tr class='center'><td class='objectListLink'>");
                text.Append(string.Concat("<a href='objects/", info.ResultFileName, "'>"));
                if (!string.IsNullOrEmpty(info.Schema))
                {
                    text.Append(string.Concat(info.Schema, "."));
                }

                text.Append(info.Name);
                text.Append("</a></td></tr>");

                if (currentLoopPos < columnSplitStart[0])
                {
                    cols[0] += text.ToString();
                }
                else if (currentLoopPos < columnSplitStart[1])
                {
                    cols[1] += text.ToString();
                }
                else
                {
                    cols[2] += text.ToString();
                }

                currentLoopPos++;
            }

            cols[0] += "</table>";
            cols[1] += "</table>";
            cols[2] += "</table>";

            return cols;
        }
    }
}
